/*
 * Copyright 2007-2009 Thomas Krause (thomas@berlin-reloaded.de)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gidoo.cms;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.jcr.AccessDeniedException;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.servlet.http.HttpServletRequest;
import javax.xml.XMLConstants;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.jackrabbit.core.TransientRepository;
import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException;
import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.nodetype.compact.CompactNodeTypeDefReader;
import org.apache.jackrabbit.core.nodetype.compact.ParseException;
import org.apache.wicket.Component;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.request.IRequestCodingStrategy;
import org.apache.wicket.request.IRequestCycleProcessor;
import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.gidoo.cms.admin.AdminPage;
import org.gidoo.cms.editor.EditorPage;
import org.gidoo.cms.login.CommonLoginPage;
import org.gidoo.cms.login.GidooCMSSession;
import org.gidoo.cms.publisher.PublisherPage;
import org.gidoo.cms.urlhandling.UrlCodingStrategy;
import org.apache.wicket.authentication.AuthenticatedWebApplication;
import org.apache.wicket.authentication.AuthenticatedWebSession;
import org.apache.wicket.extensions.ajax.markup.html.form.upload.UploadWebRequest;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.WebRequestCycleProcessor;
import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
import org.gidoo.cms.eventhandler.EventManager;
import org.gidoo.cms.jcr.JcrHelper;
import org.gidoo.cms.view.XSLTTransformFailedException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * This is the main application class. 
 * Initialization and mounting of path names is done here.
 *
 * @author <a href="mailto:thomas@berlin-reloaded.de">Thomas Krause</a>
 */
public abstract class GidooCMS extends AuthenticatedWebApplication
{

  /** The name of the logger that is used to log the user actions */
  public static String ACTION_LOG = "gidooCMS Action Logger";
  /** The name of the logger that ist used to log errors and exceptions */
  public static String ERROR_LOG = "gidooCMS Error Logger";
  private EventManager eventManager;
  private String realPathToContext;
  /** 
   * The JCR-Repository
   * @see #initRepositoryCreation() 
   */
  protected TransientRepository repository;
  /** The XSLT transformer factory. */
  private TransformerFactory tFactory;
  /** The XSLT transformer. */
  private Transformer transformer;
  /** The XML Schema validator */
  private Validator validator;
  /** The configuration for this gidooCMS. */
  public final DefaultConfiguration CMS_CONFIGURATION = createNewConfiguration();
  public final String FALLBACK_CMS_URL = "cmsFallbackSolutionDoNotUse";

  /**
   * Constructor. 
   */
  public GidooCMS()
  {
    eventManager = new EventManager();
  }

  public Class getHomePage()
  {
    if("".equals(CMS_CONFIGURATION.getCMSPath()))
    {
      return CMS_CONFIGURATION.getCMSMainPage();
    }
    else
    {
      return RedirectPage.class;
    }
  }

  /**
   * Initializes the application.
   * Mind to call <code>super.init()</code> when overriding this.
   */
  @Override
  protected void init()
  {
    super.init();

    realPathToContext = getServletContext().getRealPath("/");
    if(!realPathToContext.endsWith("/"))
    {
      realPathToContext = realPathToContext + "/";
    }

    try
    {

      initRepositoryCreation();

      Session s = repository.login(new SimpleCredentials(("cms_system"), new char[0]));

      initNodeTypeRegistration(s);
      initCreateDefaultNodes(s);
      s.logout();

    }
    catch(RepositoryException ex)
    {
      Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, null, ex);
    }
    catch(ResourceStreamNotFoundException ex)
    {
      Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, null, ex);
    }
    catch(ParseException ex)
    {
      Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, null, ex);
    }
    catch(IOException ex)
    {
      Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, null, ex);
    }
    

    initMountUrls();

    initActionLogger();
  }

  /**
   * Inits the action logger.
   * This implementation will remove all existing logging handlers and will add
   * one which is writing the output to WEB-INF/action.log .
   */
  protected void initActionLogger()
  {

    Logger actionLogger = Logger.getLogger(ACTION_LOG);
    Handler[] actionHandlers = actionLogger.getHandlers();
    for(Handler h : actionHandlers)
    {
      actionLogger.removeHandler(h);
    }
    try
    {
      File f = new File(realPathToContext + "WEB-INF/log/");
      if(!f.exists())
      {
        f.mkdir();
      }
      FileHandler fh = new FileHandler(realPathToContext + "WEB-INF/log/action.log");
      fh.setFormatter(new SimpleFormatter());
      actionLogger.addHandler(fh);
    }
    catch(Exception ex)
    {
      Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, null, ex);
      // fallback, log to console
      actionLogger.addHandler(new ConsoleHandler());
    }

  }

  /**
   * Create the reference to the JCR-Repository.
   * @see #repository
   */
  protected void initRepositoryCreation() throws IOException
  {
    String configFile = realPathToContext + "META-INF/repository.xml";
    String repHomeDir = realPathToContext + "WEB-INF/repository";

    repository = new TransientRepository(configFile, repHomeDir);
  }

  /**
   * Register the node types and namespace.
   * @param s The JCR-session.
   */
  protected void initNodeTypeRegistration(Session s) throws ResourceStreamNotFoundException, ParseException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException
  {
    // read in node type definition
    ResourceReference r = new ResourceReference(JcrHelper.class, "nodetypes.cnd");
    r.bind(this);
    InputStreamReader reader;

    reader = new InputStreamReader(r.getResource().getResourceStream().getInputStream());
    CompactNodeTypeDefReader cndReader = new CompactNodeTypeDefReader(reader,
      "nodetypes.cnd");

    List ntdList = cndReader.getNodeTypeDefs();

    // Namespaces
    NamespaceRegistry nsReg = s.getWorkspace().getNamespaceRegistry();
    try
    {
      nsReg.registerNamespace("cms", "http://gidoo.org/cms"); 
    }
    catch(NamespaceException ex)
    {
      // ignore
      //Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, "could not register namespace", ex);
    }

    NodeTypeManagerImpl manager = (NodeTypeManagerImpl) s.getWorkspace().getNodeTypeManager();
    NodeTypeRegistry ntreg = manager.getNodeTypeRegistry();

    Iterator it = ntdList.iterator();
    while(it.hasNext())
    {
      NodeTypeDef def = (NodeTypeDef) it.next();
      try
      {
        ntreg.registerNodeType(def);
      }
      catch(InvalidNodeTypeDefException ex)
      {
        //Logger.getLogger(GidooCMS.class.getName()).log(Level.SEVERE, "could not register node type", ex);
      }
    }
    
    s.save();

  }

  /**
   * Make sure all default nodes are created (if not already existant
   * 
   * @param s The JCR-session.
   */
  protected void initCreateDefaultNodes(Session s) throws RepositoryException
  {
    // add default nodes
    Node root = s.getRootNode();
    Logger.getLogger(GidooCMS.class.getName()).log(Level.INFO, "checking for default nodes");

    if(!root.hasNode("users"))
    {
      root.addNode("users");
      Logger.getLogger(GidooCMS.class.getName()).log(Level.INFO, "users node added");
    }
    if(!root.hasNode("sections"))
    {
      root.addNode("sections");
      Logger.getLogger(GidooCMS.class.getName()).log(Level.INFO, "sections node added");
    }
    if(!root.hasNode("drafts"))
    {
      root.addNode("drafts");
      Logger.getLogger(GidooCMS.class.getName()).log(Level.INFO, "drafts node added");
    }
    if(!root.hasNode("private_drafts"))
    {
      root.addNode("private_drafts");
      Logger.getLogger(GidooCMS.class.getName()).log(Level.INFO, "private_drafts node added");
    }
    root.save();

  }

  /**
   * The paths for the different parts of the application 
   * (CMS, editor UI, publishing UI, adminstration UI) are mounted here using
   * the configuration variables which were set by the constructor.<br><br>
   * 
   * Please always call <code>super()</code> first if you want to add some additional paths.
   */
  protected void initMountUrls()
  {

    if("".equals(CMS_CONFIGURATION.getCMSPath()))
    {
      mount(new UrlCodingStrategy(CMS_CONFIGURATION.getCMSMainPage(), FALLBACK_CMS_URL));
    }
    else
    {
      mount(new UrlCodingStrategy(CMS_CONFIGURATION.getCMSMainPage(),
        CMS_CONFIGURATION.getCMSPath()));
    }
    mountBookmarkablePage(CMS_CONFIGURATION.getCMSEditPath(), EditorPage.class);
    mountBookmarkablePage(CMS_CONFIGURATION.getCMSPublishPath(), PublisherPage.class);
    mountBookmarkablePage(CMS_CONFIGURATION.getCMSAdminPath(), AdminPage.class);
  }

  protected Class<? extends AuthenticatedWebSession> getWebSessionClass()
  {
    return GidooCMSSession.class;
  }

  protected Class<? extends WebPage> getSignInPageClass()
  {
    return CommonLoginPage.class;
  }

  @Override
  protected IRequestCycleProcessor newRequestCycleProcessor()
  {
    return new WebRequestCycleProcessor()
    {

      @Override
      protected IRequestCodingStrategy newRequestCodingStrategy()
      {
        return new WebRequestCodingStrategy()
        {

          @Override
          public IRequestTargetUrlCodingStrategy urlCodingStrategyForPath(String path)
          {
            String result = path;
            if("".equals(CMS_CONFIGURATION.getCMSPath()))
            {

              if(!path.startsWith(CMS_CONFIGURATION.getCMSAdminPath()) && !path.startsWith(CMS_CONFIGURATION.getCMSEditPath()) && !path.startsWith(CMS_CONFIGURATION.getCMSPublishPath()) && isCMSContent(path))
              {
                result = FALLBACK_CMS_URL + "/" + result;
              }
            }
            return super.urlCodingStrategyForPath(result);
          }
        };
      }
    };
  }

  /** 
   * Returns the path of this context on the file system of the server.
   * Always ends with an "/".
   */
  public String getRealPathToContext()
  {
    return realPathToContext;
  }

  /** Get the event manager. */
  public EventManager getEventManager()
  {
    return eventManager;
  }

  /** Return the JCR repository of the web application. */
  public Repository getRepository()
  {
    return repository;
  }

  /**
   * Parses XML and validates the content
   * @param newContent The XML content as string
   * @param comp  The component (used for error handling), may be null
   */
  public boolean validate(String newContent, final Component comp)
  {
    try
    {
      if(validator == null)
      {
        File f = new File(getRealPathToContext() + "/WEB-INF/xml/markup.xsd");
        if(!f.exists())
        {
          if(comp != null)
          {
            comp.error(comp.getString("missingschemafile"));
          }
          return false;
        }

        Schema s =
          SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(f);

        validator = s.newValidator();
      }

      StringReader reader = new StringReader(newContent);

      StreamSource source = new StreamSource(reader);

      validator.validate(source);

    }
    catch(SAXException ex)
    {
      if(comp != null && ex instanceof SAXParseException)
      {
        SAXParseException exception = (SAXParseException) ex;

        String msg = MessageFormat.format(comp.getString("validationfailed"),
          exception.getLineNumber(),
          exception.getColumnNumber());
        comp.error(msg);
        comp.error(exception.getLocalizedMessage());
      }

      return false;
    }
    catch(IOException ex)
    {
      if(comp != null)
      {
        comp.error(comp.getString("error"));
      }
      return false;
    }

    return true;
  }

  /** Parses the XML and produces the content. */
  public String transformContent(String input)
    throws XSLTTransformFailedException
  {
    try
    {
      if(transformer == null || tFactory == null)
      {
        tFactory = TransformerFactory.newInstance();

        Logger.getLogger(GidooCMS.class.getName()).log(
          Level.INFO, "reading in the XSLT-Script");

        File xsltScript = new File(getRealPathToContext() + "/WEB-INF/xml/content.xsl");
        if(!xsltScript.exists())
        {
          throw (new XSLTTransformFailedException("The XSLT-script could not be found."));
        }

        transformer = tFactory.newTransformer(new StreamSource(xsltScript));
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

      }

      StringWriter w = new StringWriter();

      transformer.transform(new StreamSource(new StringReader(input)), new StreamResult(w));

      return w.toString();
    }
    catch(Exception ex)
    {
      throw (new XSLTTransformFailedException(ex.getLocalizedMessage()));
    }
  }

  /**
   * Return true if the given URL (relative to context)
   * is a dynamic part of your cms. Whenever you have an empty CMS path you
   * should override this function in order to divide between "managed" dynamic
   * content and static content of your site.<br>
   * Please note that manually mounted URLs also should return <code>false</code>
   *
   * @see DefaultConfiguration#getCMSPath()
   * @see #initMountUrls() 
   */
  public boolean isCMSContent(String url)
  {
    return true;
  }

  /** 
   * You have to override this function in order to return a new 
   * configuration object.
   * 
   * @see DefaultConfiguration
   */
  public abstract DefaultConfiguration createNewConfiguration();

  @Override
  protected void onDestroy()
  {
    super.onDestroy();
    if(repository != null)
    {
      Logger.getAnonymousLogger().log(Level.INFO, "shutting down the JCR repository");
      repository.shutdown();
    }
  }

  @Override
  protected WebRequest newWebRequest(HttpServletRequest servletRequest)
  {
    return new UploadWebRequest(servletRequest);
  }



}
